// Copyright (c) 2009 Mike Haight, Andy Kipp
//
// Permission is hereby granted, free of charge, to any person
// obtaining a copy of this software and associated documentation
// files (the "Software"), to deal in the Software without
// restriction, including without limitation the rights to use,
// copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following
// conditions:
//
// The above copyright notice and this permission notice shall be
// included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
// OTHER DEALINGS IN THE SOFTWARE.

#include "stdafx.h"


Renderer::Renderer(Window^ argWindow) : mWindow(argWindow)
{
    mWindowSized = gcnew SizedEventHandler(this, &Renderer::WindowResized);
    Start();
    mWindow->Sized += mWindowSized;
}


Renderer::~Renderer(void)
{
    mWindow->Sized -= mWindowSized;
    Stop();
}


#ifdef _D3D10_1

//
// Public Methods.
//

void Renderer::BeginDrawing(void)
{
    mSprite->Begin(D3DX10_SPRITE_SORT_TEXTURE);
}


void Renderer::Clear(void)
{
    static const FLOAT clearColor[] = { 0.0, 0.0, 0.0, 1.0 };
    mDevice->ClearRenderTargetView(mRenderTargetView, clearColor);
}


IntPtr Renderer::CreateTexture(String^ argTextureFile)
{
    if (!File::Exists(argTextureFile))
        throw gcnew ArgumentException(String::Format("The specified file does not exist: {0}", argTextureFile));

    IntPtr textureFile = System::Runtime::InteropServices::Marshal::StringToHGlobalUni(argTextureFile);
    ID3D10ShaderResourceView* shaderResourceView = NULL;

    // Create Texture Resource
    try
    {
        ID3D10Resource* resource = NULL;

        Marshal::ThrowExceptionForHR(
            D3DX10CreateTextureFromFileW(
                (mDevice),
                (LPCWSTR)textureFile.ToPointer(),
                (NULL),
                (NULL),
                (&resource),
                (NULL)));

        // Get Texture Description
        ID3D10Texture2D* texture = (ID3D10Texture2D*)resource;

        // Create Resource View
        try
        {
            D3D10_TEXTURE2D_DESC desc;
            texture->GetDesc(&desc);

            D3D10_SHADER_RESOURCE_VIEW_DESC SRVDesc;
            ZeroMemory(&SRVDesc, sizeof(SRVDesc));
            SRVDesc.Format = desc.Format;
            SRVDesc.ViewDimension = D3D10_SRV_DIMENSION_TEXTURE2D;
            SRVDesc.TextureCube.MipLevels = desc.MipLevels;
            SRVDesc.TextureCube.MostDetailedMip = 0;

            Marshal::ThrowExceptionForHR(
                mDevice->CreateShaderResourceView(
                    texture,
                    &SRVDesc,
                    &shaderResourceView));
        }
        finally
        {
            texture->Release();
        }
    }
    finally
    {
        System::Runtime::InteropServices::Marshal::FreeHGlobal(textureFile);
    }

    return IntPtr(shaderResourceView);
}


void Renderer::Draw(List<Sprite^>^ argSprites)
{
    // Create the sprite pool as a static array, we do not want to heap allocate each time
    static const int SpritePoolSize = 64;
    // sizeof(spritePool) = sizeof(D3DX10_SPRITE) * SpritePoolSize = 104 bytes * 64 = 6.5 kilobytes
    static D3DX10_SPRITE spritePool[SpritePoolSize];

    static D3DXMATRIX matrixScaling;
    static D3DXMATRIX matrixTranslation;

    int poolIndex = 0;
    for each (Sprite^ sprite in argSprites)
    {
        ::D3DXMatrixScaling(
            &matrixScaling,
            2 * sprite->Width / mViewRectangle.Width,
            2 * sprite->Height / mViewRectangle.Height,
            1.0f);

        ::D3DXMatrixTranslation(
            &matrixTranslation,
            (2*sprite->X + sprite->Width - 2*mViewRectangle.X) / mViewRectangle.Width - 1,
            (-2*sprite->Y - sprite->Height + 2*mViewRectangle.Y) / mViewRectangle.Height + 1,
            0.0f);

        static D3DXMATRIX matrix;
        ::D3DXMatrixIdentity(&matrix);

        // Get the sprite coord, size, and texture
        Frame currentFrame = sprite->Texture->GetFrame(sprite->Frame);
        spritePool[poolIndex].matWorld = matrixScaling * matrixTranslation;
        spritePool[poolIndex].TexCoord.x = currentFrame.TextureX;
        spritePool[poolIndex].TexCoord.y = currentFrame.TextureY;
        spritePool[poolIndex].TexSize.x = currentFrame.TextureWidth;
        spritePool[poolIndex].TexSize.y = currentFrame.TextureHeight;
        spritePool[poolIndex].ColorModulate = sprite->ColorModulation;
        spritePool[poolIndex].pTexture = (ID3D10ShaderResourceView*)sprite->Texture->NativeTexturePointer.ToPointer();
        spritePool[poolIndex].TextureIndex = 0;

        // If the poolIndex has become greater than our buffer size, then we
        // must draw the sprites contained therein, and reset the poolIndex to
        // zero.  Note that if the number of sprites in the list is not an even
        // increment of the buffer size, then this does not draw the last chunk
        // of sprites
        if (++poolIndex >= SpritePoolSize)
        {
            // MSDN
            // http://msdn.microsoft.com/en-us/library/bb205604(VS.85).aspx
            //
            // This draw method is most useful when drawing a large number of
            // sprites that have already been sorted on the CPU (or do not need
            // to be sorted), such as in a particle system.
            //
            // We are using DrawSpritesImmediate() and not DrawSpritesBuffered()
            // because we do not want or need the sprites to be sorted in any
            // way.
            mSprite->DrawSpritesImmediate(spritePool, poolIndex, NULL, NULL);
            poolIndex = 0;
        }
    }

    // If there are sprites remaining in the buffer because of an uneven
    // increment, we must draw them out
    if (poolIndex > 0)
    {
        mSprite->DrawSpritesImmediate(spritePool, poolIndex, NULL, NULL);
    }
}


void Renderer::EndDrawing(void)
{
    mSprite->End();
}


void Renderer::Present(void)
{
    Marshal::ThrowExceptionForHR(mSwapChain->Present(0, 0));
}


void Renderer::SetView(Coordinates::Rectangle argRectangle)
{
    mViewRectangle = argRectangle;
}


//
// Private Methods.
//

void Renderer::Start(void)
{
    // Create the D3D10.1 device
    ID3D10BlendState*       blendState = NULL;
    ID3D10Device1*          device = NULL;
    IDXGISwapChain*         dxgiSwapChain = NULL;
    ID3D10RenderTargetView* renderTargetView = NULL;
    ID3DX10Sprite*          sprite = NULL;


    {
        array<DriverType>^ driverTypes =
        {
            DriverType::Hardware,
            //Unsupported: DriverType::Warp,
            //Unsupported: DriverType::Reference,
        };

        // These are the feature levels to try depending on the feature level of the graphics card
        array<FeatureLevel>^ featureLevels =
        {
            FeatureLevel::Ten_1,
            FeatureLevel::Ten_0,
            FeatureLevel::Nine_3,
            FeatureLevel::Nine_2,
            FeatureLevel::Nine_1,
        };

        // The swap chain description
        DXGI_SWAP_CHAIN_DESC swapDesc = {0};
        swapDesc.BufferDesc.Width = mWindow->ViewWidth;
        swapDesc.BufferDesc.Height = mWindow->ViewHeight;
        swapDesc.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
        swapDesc.SampleDesc.Count = 1;
        swapDesc.SampleDesc.Quality = 0;
        swapDesc.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
        swapDesc.BufferCount = 1;
        swapDesc.OutputWindow = (HWND)mWindow->Handle.ToPointer();
        swapDesc.Windowed = TRUE;

        // Device creation flags
        #ifdef _DEBUG
            UINT flags = D3D10_CREATE_DEVICE_DEBUG;
        #else
            UINT flags = 0;
        #endif

        HRESULT hResult = E_FAIL;
        for each (DriverType driverType in driverTypes)
        {
            for each (FeatureLevel featureLevel in featureLevels)
            {
                hResult = ::D3D10CreateDeviceAndSwapChain1(
                    (NULL),
                    (D3D10_DRIVER_TYPE)driverType,
                    (NULL),
                    (flags),
                    (D3D10_FEATURE_LEVEL1)featureLevel,
                    (D3D10_1_SDK_VERSION),
                    (&swapDesc),
                    (&dxgiSwapChain),
                    (&device));

                if (SUCCEEDED(hResult))
                {
                    smLogger->Info("Created D3D10.1 device using driver type {0} and feature level {1}", driverType, featureLevel);
                    break;
                }
            }

            if (SUCCEEDED(hResult))
            {
                break;
            }
        }

        // If we ended up failing all of the device creation attempts
        Marshal::ThrowExceptionForHR(hResult);
    }

    // Get/Set settings on the device
    {
        device->IASetPrimitiveTopology(D3D10_PRIMITIVE_TOPOLOGY_TRIANGLELIST);

        // Set the view port
        {
            D3D10_VIEWPORT viewPort;
            viewPort.Width = mWindow->ViewWidth;
            viewPort.Height = mWindow->ViewHeight;
            viewPort.MinDepth = 0.0f;
            viewPort.MaxDepth = 1.0f;
            viewPort.TopLeftX = 0;
            viewPort.TopLeftY = 0;
            device->RSSetViewports(1, &viewPort);
        }
    }

    // Set the Output Manager blend state
    {
        D3D10_BLEND_DESC blendDescription = {0};

        blendDescription.AlphaToCoverageEnable = FALSE;
        blendDescription.BlendEnable[0] = TRUE;

        // All BlendEnable values must be the same for D3D10_FEATURE_LEVEL_9_*
        if (!(device->GetFeatureLevel() > D3D10_FEATURE_LEVEL_9_3))
        {
            blendDescription.BlendEnable[1] = TRUE;
            blendDescription.BlendEnable[2] = TRUE;
            blendDescription.BlendEnable[3] = TRUE;
            blendDescription.BlendEnable[4] = TRUE;
            blendDescription.BlendEnable[5] = TRUE;
            blendDescription.BlendEnable[6] = TRUE;
            blendDescription.BlendEnable[7] = TRUE;
        }

        blendDescription.SrcBlend = D3D10_BLEND_SRC_ALPHA;
        blendDescription.DestBlend = D3D10_BLEND_INV_SRC_ALPHA;
        blendDescription.SrcBlendAlpha = D3D10_BLEND_SRC_ALPHA;
        blendDescription.DestBlendAlpha = D3D10_BLEND_INV_SRC_ALPHA;
        blendDescription.BlendOp = D3D10_BLEND_OP_ADD;
        blendDescription.BlendOpAlpha = D3D10_BLEND_OP_ADD;
        blendDescription.RenderTargetWriteMask[0] = D3D10_COLOR_WRITE_ENABLE_ALL;

        Marshal::ThrowExceptionForHR(
            device->CreateBlendState(&blendDescription, &blendState));

        device->OMSetBlendState(blendState, NULL, 0xFFFFFFFF);
    }

    // Get the first swapchain buffer for drawing
    {
        ID3D10Texture2D* backBuffer = NULL;

        Marshal::ThrowExceptionForHR(
            dxgiSwapChain->GetBuffer(0, __uuidof(ID3D10Texture2D), reinterpret_cast<void**>(&backBuffer)));

        // Get the render target
        try
        {
            Marshal::ThrowExceptionForHR(
                device->CreateRenderTargetView(
                    backBuffer,
                    NULL,
                    &renderTargetView));

            device->OMSetRenderTargets(1, &renderTargetView, NULL);
        }
        finally
        {
            SAFE_RELEASE(backBuffer);
        }
    }

    // Create a sprite
    {
        Marshal::ThrowExceptionForHR(
            ::D3DX10CreateSprite(
                device,
                1,
                &sprite));
    }

    // Save the pointers
    {
        mBlendState =       blendState;
        mDevice =           device;
        mSwapChain =        dxgiSwapChain;
        mRenderTargetView = renderTargetView;
        mSprite =           sprite;
    }
}


void Renderer::Stop(void)
{
    if (NULL != mDevice)
    {
        mDevice->ClearState();
    }

    SAFE_RELEASE(mSprite);
    SAFE_RELEASE(mRenderTargetView);
    SAFE_RELEASE(mSwapChain);
    SAFE_RELEASE(mBlendState);
    SAFE_RELEASE(mDevice);
}


void Renderer::WindowResized(Object^ argSender, SizedEventArgs^ e)
{
   // Set the view port
    {
        D3D10_VIEWPORT viewPort;
        viewPort.Width = e->Width;
        viewPort.Height = e->Height;
        viewPort.MinDepth = 0.0f;
        viewPort.MaxDepth = 1.0f;
        viewPort.TopLeftX = 0;
        viewPort.TopLeftY = 0;
        mDevice->RSSetViewports(1, &viewPort);
    }

    // Resize the buffers
    Marshal::ThrowExceptionForHR(
        mSwapChain->ResizeBuffers(
            1,
            e->Width,
            e->Height,
            DXGI_FORMAT_R8G8B8A8_UNORM,
            0));
}

#else
#ifdef _D3D9

//
// Public Methods.
//

void Renderer::BeginDrawing(void)
{
    mDevice->BeginScene();
    mSprite->Begin(NULL);
}


void Renderer::Clear(void)
{
    static const D3DCOLOR clearColor = D3DCOLOR_ARGB(1, 0, 0, 0);
    mDevice->Clear( 0, NULL, D3DCLEAR_TARGET, clearColor, 1.0f, 0 );
}


IntPtr Renderer::CreateTexture(String^ argTextureFile)
{
    IntPtr textureFile = System::Runtime::InteropServices::Marshal::StringToHGlobalUni(argTextureFile);

    IDirect3DTexture9 *tex = NULL;

    // Create Texture
    try
    {
        Marshal::ThrowExceptionForHR(
            D3DXCreateTextureFromFileEx(
            mDevice,
            (LPCWSTR)textureFile.ToPointer(),
             D3DX_DEFAULT,
             D3DX_DEFAULT,
             D3DX_DEFAULT,
             NULL,
             D3DFMT_UNKNOWN,
             D3DPOOL_MANAGED,
             D3DX_DEFAULT,
             D3DX_DEFAULT,
             D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f),
             NULL,
             NULL,
             &tex));
    }
    finally
    {
        System::Runtime::InteropServices::Marshal::FreeHGlobal(textureFile);
    }

    // Get Texture Description
    D3DSURFACE_DESC desc;
    tex->GetLevelDesc(0, &desc);

    // Return TexturePtr
    return IntPtr(tex);
}


void Renderer::Draw(List<Sprite^>^ argSprites)
{
    for each (Sprite^ sprite in argSprites)
    {
        D3DXMATRIX matrixScaling;
        ::D3DXMatrixScaling(
            &matrixScaling,
            1.0f,
            1.0f,
            0.0f);

        D3DXMATRIX matrixTranslation;
        ::D3DXMatrixTranslation(
            &matrixTranslation,
            (float)(sprite->X - mViewRectangle.X),
            (float)(sprite->Y - mViewRectangle.Y),
            0.0f);

        D3DXMATRIX matrixTransform = matrixScaling * matrixTranslation;
        mSprite->SetTransform(&matrixTransform);

        // Get Sprite current Frame
        int textureHeight = sprite->Texture->Height;
        int textureWidth = sprite->Texture->Width;
        Frame currentFrame = sprite->Texture->GetFrame(sprite->Frame);

        RECT textureRect;
        textureRect.left =      (LONG)(currentFrame.TextureX * textureWidth);
        textureRect.top =       (LONG)(currentFrame.TextureY * textureHeight);
        textureRect.bottom =    (LONG)(textureRect.top + (currentFrame.TextureHeight * textureHeight));
        textureRect.right =     (LONG)(textureRect.left + (currentFrame.TextureWidth * textureWidth));

        mSprite->Draw(
            (IDirect3DTexture9*)sprite->Texture->NativeTexturePointer.ToPointer(),
            &textureRect,
            NULL,
            NULL,
            sprite->ColorModulation);

    }
}


void Renderer::EndDrawing(void)
{
    mSprite->End();
    mDevice->EndScene();
}


void Renderer::Present(void)
{
    Marshal::ThrowExceptionForHR(
        mDevice->Present(NULL, NULL, NULL, NULL));
}


void Renderer::SetView(Coordinates::Rectangle argRectangle)
{
    mViewRectangle = argRectangle;
}


//
// Private Methods.
//

void Renderer::Start(void)
{
    IDirect3D9* d3d9 = NULL;
    IDirect3DDevice9* device = NULL;

    // Create D3D9 Interface
    if (NULL == (d3d9 = Direct3DCreate9(D3D_SDK_VERSION)))
    {
        throw gcnew Exception("Error creating D3D9 Interface.");
    }

    // Get Adapter Display Mode
    D3DDISPLAYMODE d3ddm;
    Marshal::ThrowExceptionForHR(
        d3d9->GetAdapterDisplayMode(
            D3DADAPTER_DEFAULT,
            &d3ddm));

    // Check Device Supports Adapter Display Mode
    Marshal::ThrowExceptionForHR(
        d3d9->CheckDeviceFormat(
            D3DADAPTER_DEFAULT,
            D3DDEVTYPE_HAL,
            d3ddm.Format,
            D3DUSAGE_DEPTHSTENCIL,
            D3DRTYPE_SURFACE,
            D3DFMT_D16));

    // Get Adapter Capabilties
    D3DCAPS9 d3dCaps;
    Marshal::ThrowExceptionForHR(
        d3d9->GetDeviceCaps(
            D3DADAPTER_DEFAULT,
            D3DDEVTYPE_HAL,
            &d3dCaps));

    // Create D3D9 Device
    {
        D3DPRESENT_PARAMETERS d3dpp;
        ZeroMemory(&d3dpp, sizeof(d3dpp));
        d3dpp.BackBufferFormat       = d3ddm.Format;
        d3dpp.SwapEffect             = D3DSWAPEFFECT_DISCARD;
        d3dpp.Windowed               = TRUE;
        d3dpp.PresentationInterval   = D3DPRESENT_INTERVAL_IMMEDIATE;

        Marshal::ThrowExceptionForHR(
            d3d9->CreateDevice(
                D3DADAPTER_DEFAULT,
                D3DDEVTYPE_HAL,
                (HWND)mWindow->Handle.ToPointer(),
                D3DCREATE_SOFTWARE_VERTEXPROCESSING,
                &d3dpp,
                &device));

        // Setup Alpha Blending
        {
            device->SetRenderState(D3DRS_ALPHABLENDENABLE, TRUE);
            device->SetRenderState(D3DRS_SRCBLEND, D3DBLEND_SRCALPHA);
            device->SetRenderState(D3DRS_DESTBLEND, D3DBLEND_INVSRCALPHA);
            device->SetTextureStageState(0, D3DTSS_ALPHAARG1,D3DTA_TEXTURE);
        }
    }

    // Create D3D9 Sprite
    ID3DXSprite* sprite = NULL;
    {
        Marshal::ThrowExceptionForHR(
            ::D3DXCreateSprite(
                device,
                &sprite));
    }

    // Create D3D9 Viewport
    {
        D3DVIEWPORT9 viewPort;
        viewPort.X =        0;
        viewPort.Y =        0;
        viewPort.Width =    mWindow->ViewWidth;
        viewPort.Height =   mWindow->ViewHeight;
        viewPort.MinZ =     0.0f;
        viewPort.MaxZ =     1.0f;
        device->SetViewport(&viewPort);
    }

    {
        mInterface = d3d9;
        mDevice = device;
        mSprite = sprite;
    }

    smLogger->Info("Created D3D9 device");
}


void Renderer::Stop(void)
{
    SAFE_RELEASE(mSprite);
    SAFE_RELEASE(mDevice);
    SAFE_RELEASE(mInterface);
}


void Renderer::WindowResized(Object^ argSender, SizedEventArgs^ e)
{
    // mDevice->Reset();
}

#endif /* _D3D9 */
#endif /* _D3D10_1 */